import { ColumnMetadata } from "./ColumnMetadata"
import { RelationMetadata } from "./RelationMetadata"
import { EntityMetadata } from "./EntityMetadata"
import { EmbeddedMetadataArgs } from "../metadata-args/EmbeddedMetadataArgs"
import { RelationIdMetadata } from "./RelationIdMetadata"
import { RelationCountMetadata } from "./RelationCountMetadata"
import { DataSource } from "../data-source/DataSource"
import { EntityListenerMetadata } from "./EntityListenerMetadata"
import { IndexMetadata } from "./IndexMetadata"
import { UniqueMetadata } from "./UniqueMetadata"
import { TypeORMError } from "../error"

/**
 * Contains all information about entity's embedded property.
 */
export class EmbeddedMetadata {
    // ---------------------------------------------------------------------
    // Public Properties
    // ---------------------------------------------------------------------

    /**
     * Entity metadata where this embedded is.
     */
    entityMetadata: EntityMetadata

    /**
     * Parent embedded in the case if this embedded inside other embedded.
     */
    parentEmbeddedMetadata?: EmbeddedMetadata

    /**
     * Embedded target type.
     */
    type: Function | string

    /**
     * Property name on which this embedded is attached.
     */
    propertyName: string

    /**
     * Gets full path to this embedded property (including embedded property name).
     * Full path is relevant when embedded is used inside other embeds (one or multiple nested).
     * For example it will return "counters.subcounters".
     */
    propertyPath: string

    /**
     * Columns inside this embed.
     */
    columns: ColumnMetadata[] = []

    /**
     * Relations inside this embed.
     */
    relations: RelationMetadata[] = []

    /**
     * Entity listeners inside this embed.
     */
    listeners: EntityListenerMetadata[] = []

    /**
     * Indices applied to the embed columns.
     */
    indices: IndexMetadata[] = []

    /**
     * Uniques applied to the embed columns.
     */
    uniques: UniqueMetadata[] = []

    /**
     * Relation ids inside this embed.
     */
    relationIds: RelationIdMetadata[] = []

    /**
     * Relation counts inside this embed.
     */
    relationCounts: RelationCountMetadata[] = []

    /**
     * Nested embeddable in this embeddable (which has current embedded as parent embedded).
     */
    embeddeds: EmbeddedMetadata[] = []

    /**
     * Indicates if the entity should be instantiated using the constructor
     * or via allocating a new object via `Object.create()`.
     */
    isAlwaysUsingConstructor: boolean = true

    /**
     * Indicates if this embedded is in array mode.
     *
     * This option works only in mongodb.
     */
    isArray: boolean = false

    /**
     * Prefix of the embedded, used instead of propertyName.
     * If set to empty string or false, then prefix is not set at all.
     */
    customPrefix: string | boolean | undefined

    /**
     * Gets the prefix of the columns.
     * By default its a property name of the class where this prefix is.
     * But if custom prefix is set then it takes its value as a prefix.
     * However if custom prefix is set to empty string or false, then prefix to column is not applied at all.
     */
    prefix: string

    /**
     * Returns array of property names of current embed and all its parent embeds.
     *
     * example: post[data][information][counters].id where "data", "information" and "counters" are embeds
     * we need to get value of "id" column from the post real entity object.
     * this method will return ["data", "information", "counters"]
     */
    parentPropertyNames: string[] = []

    /**
     * Returns array of prefixes of current embed and all its parent embeds.
     */
    parentPrefixes: string[] = []

    /**
     * Returns embed metadatas from all levels of the parent tree.
     *
     * example: post[data][information][counters].id where "data", "information" and "counters" are embeds
     * this method will return [embed metadata of data, embed metadata of information, embed metadata of counters]
     */
    embeddedMetadataTree: EmbeddedMetadata[] = []

    /**
     * Embed metadatas from all levels of the parent tree.
     *
     * example: post[data][information][counters].id where "data", "information" and "counters" are embeds
     * this method will return [embed metadata of data, embed metadata of information, embed metadata of counters]
     */
    columnsFromTree: ColumnMetadata[] = []

    /**
     * Relations of this embed and all relations from its child embeds.
     */
    relationsFromTree: RelationMetadata[] = []

    /**
     * Relations of this embed and all relations from its child embeds.
     */
    listenersFromTree: EntityListenerMetadata[] = []

    /**
     * Indices of this embed and all indices from its child embeds.
     */
    indicesFromTree: IndexMetadata[] = []

    /**
     * Uniques of this embed and all uniques from its child embeds.
     */
    uniquesFromTree: UniqueMetadata[] = []

    /**
     * Relation ids of this embed and all relation ids from its child embeds.
     */
    relationIdsFromTree: RelationIdMetadata[] = []

    /**
     * Relation counts of this embed and all relation counts from its child embeds.
     */
    relationCountsFromTree: RelationCountMetadata[] = []

    // ---------------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------------

    constructor(options: {
        entityMetadata: EntityMetadata
        args: EmbeddedMetadataArgs
    }) {
        this.entityMetadata = options.entityMetadata
        this.type = options.args.type()
        this.propertyName = options.args.propertyName
        this.customPrefix = options.args.prefix
        this.isArray = options.args.isArray
    }

    // ---------------------------------------------------------------------
    // Public Methods
    // ---------------------------------------------------------------------

    /**
     * Creates a new embedded object.
     */
    create(options?: { fromDeserializer?: boolean }): any {
        if (!(typeof this.type === "function")) {
            return {}
        }

        if (options?.fromDeserializer || !this.isAlwaysUsingConstructor) {
            return Object.create(this.type.prototype)
        } else {
            return new (this.type as any)()
        }
    }

    // ---------------------------------------------------------------------
    // Builder Methods
    // ---------------------------------------------------------------------

    build(connection: DataSource): this {
        this.embeddeds.forEach((embedded) => embedded.build(connection))
        this.prefix = this.buildPrefix(connection)
        this.parentPropertyNames = this.buildParentPropertyNames()
        this.parentPrefixes = this.buildParentPrefixes()
        this.propertyPath = this.parentPropertyNames.join(".")
        this.embeddedMetadataTree = this.buildEmbeddedMetadataTree()
        this.columnsFromTree = this.buildColumnsFromTree()
        this.relationsFromTree = this.buildRelationsFromTree()
        this.listenersFromTree = this.buildListenersFromTree()
        this.indicesFromTree = this.buildIndicesFromTree()
        this.uniquesFromTree = this.buildUniquesFromTree()
        this.relationIdsFromTree = this.buildRelationIdsFromTree()
        this.relationCountsFromTree = this.buildRelationCountsFromTree()

        if (connection.options.entitySkipConstructor) {
            this.isAlwaysUsingConstructor =
                !connection.options.entitySkipConstructor
        }

        return this
    }

    // ---------------------------------------------------------------------
    // Protected Methods
    // ---------------------------------------------------------------------

    protected buildPartialPrefix(): string[] {
        // if prefix option was not set or explicitly set to true - default prefix
        if (this.customPrefix === undefined || this.customPrefix === true) {
            return [this.propertyName]
        }

        // if prefix option was set to empty string or explicity set to false - disable prefix
        if (this.customPrefix === "" || this.customPrefix === false) {
            return []
        }

        // use custom prefix
        if (typeof this.customPrefix === "string") {
            return [this.customPrefix]
        }

        throw new TypeORMError(
            `Invalid prefix option given for ${this.entityMetadata.targetName}#${this.propertyName}`,
        )
    }

    protected buildPrefix(connection: DataSource): string {
        if (connection.driver.options.type === "mongodb")
            return this.propertyName

        let prefixes: string[] = []
        if (this.parentEmbeddedMetadata)
            prefixes.push(this.parentEmbeddedMetadata.buildPrefix(connection))

        prefixes.push(...this.buildPartialPrefix())

        return prefixes.join("_") // todo: use naming strategy instead of "_"  !!!
    }

    protected buildParentPropertyNames(): string[] {
        return this.parentEmbeddedMetadata
            ? this.parentEmbeddedMetadata
                  .buildParentPropertyNames()
                  .concat(this.propertyName)
            : [this.propertyName]
    }

    protected buildParentPrefixes(): string[] {
        return this.parentEmbeddedMetadata
            ? this.parentEmbeddedMetadata
                  .buildParentPrefixes()
                  .concat(this.buildPartialPrefix())
            : this.buildPartialPrefix()
    }

    protected buildEmbeddedMetadataTree(): EmbeddedMetadata[] {
        return this.parentEmbeddedMetadata
            ? this.parentEmbeddedMetadata
                  .buildEmbeddedMetadataTree()
                  .concat(this)
            : [this]
    }

    protected buildColumnsFromTree(): ColumnMetadata[] {
        return this.embeddeds.reduce(
            (columns, embedded) =>
                columns.concat(embedded.buildColumnsFromTree()),
            this.columns,
        )
    }

    protected buildRelationsFromTree(): RelationMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildRelationsFromTree()),
            this.relations,
        )
    }

    protected buildListenersFromTree(): EntityListenerMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildListenersFromTree()),
            this.listeners,
        )
    }

    protected buildIndicesFromTree(): IndexMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildIndicesFromTree()),
            this.indices,
        )
    }

    protected buildUniquesFromTree(): UniqueMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildUniquesFromTree()),
            this.uniques,
        )
    }

    protected buildRelationIdsFromTree(): RelationIdMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildRelationIdsFromTree()),
            this.relationIds,
        )
    }

    protected buildRelationCountsFromTree(): RelationCountMetadata[] {
        return this.embeddeds.reduce(
            (relations, embedded) =>
                relations.concat(embedded.buildRelationCountsFromTree()),
            this.relationCounts,
        )
    }
}
